11장. 이벤트는 항상 순서대로 오지 않는다
10장에서 우리는
정합성을 단계적으로 맞추는 설계를 배웠다.
하지만 그 설계에는 중요한 전제가 있다.
이벤트가 우리가 기대하는 순서로 도착한다.
현실은 다르다.
- 네트워크 지연
- 재시도
- Consumer 재시작
- 브로커 리밸런싱
- 병렬 처리
이 모든 요인이
이벤트 순서를 뒤섞을 수 있다.
왜 순서가 깨지는가
이벤트 흐름은 단순해 보인다.
flowchart LR
Producer --> Broker --> Consumer
하지만 실제 환경에서는 다음과 같은 일이 발생한다.
1️⃣ 네트워크 지연
먼저 발생한 이벤트가
나중에 도착할 수 있다.
2️⃣ 재시도
ACK 손실이나 타임아웃으로 인해
이전 이벤트가 뒤늦게 재전송될 수 있다.
3️⃣ 병렬 소비
여러 Consumer가 동시에 처리하면
완료 순서가 달라질 수 있다.
즉,
발생 순서와 처리 완료 순서는 다를 수 있다.
브로커 레벨에서의 순서 보장 (대부분의 경우 해결)
다행히 대부분의 메시지 브로커는
제한적인 범위 안에서 순서를 보장한다.
하지만 중요한 점은 이것이다.
순서 보장은 항상 “범위 제한적”이다.
1️⃣ Kafka — 파티션 단위 순서
Kafka는 파티션 내부에서만 순서를 보장한다.
같은 Key를 같은 파티션으로 보내면
그 Key에 대해서는 순서가 유지된다.
핵심 전략:
같은 비즈니스 키는 같은 파티션으로 보낸다.
예:
- 주문 ID를 파티션 키로 사용
- 사용자 ID를 파티션 키로 사용
주의:
- 다른 파티션 간 순서는 보장되지 않는다.
- 파티션 수를 변경하면 분배 전략이 달라질 수 있다.
2️⃣ AWS SQS FIFO — Message Group ID
SQS FIFO는
같은 Message Group ID 내에서 순서를 보장한다.
- 같은 Group ID → 순서 유지
- 다른 Group ID → 순서 보장 없음
하지만 Group 단위로 직렬 처리되기 때문에
병렬성이 일부 제한된다.
3️⃣ RabbitMQ
RabbitMQ는 큐 내부에서는
전달 순서를 유지한다.
그러나:
- 여러 Consumer가 병렬로 처리하면
- ACK 시점에 따라 체감 순서가 달라질 수 있다.
엄격한 순서를 원한다면:
- 단일 Consumer 사용
- Prefetch 조정
- 병렬 처리 제한
이 필요하다.
4️⃣ Google Cloud Pub/Sub
Pub/Sub은 Ordering Key를 제공한다.
같은 Ordering Key를 사용하면
그 키 단위로 순서를 유지한다.
단, 장애나 재시도 상황에서는
중복 및 재정렬 가능성을 고려해야 한다.
⚠️ 브로커 순서 보장이 성립하려면
브로커 설정만으로 순서가 보장되는 것은 아니다.
다음 조건이 충족되어야 한다.
1️⃣ 같은 키는 반드시 같은 스트림으로 묶여야 한다
- Kafka → 같은 Key는 같은 파티션
- SQS FIFO → 같은 Message Group ID
- Pub/Sub → 같은 Ordering Key
키가 흔들리면
순서 보장은 즉시 깨진다.
2️⃣ 해당 스트림은 동시에 하나만 처리해야 한다
“소비자가 하나여야 한다”는 뜻은 아니다.
정확히는:
같은 파티션(또는 Group/Ordering Key)은
동시에 하나의 소비자만 처리해야 한다.
Kafka는 consumer group 내에서
파티션을 하나의 consumer에게만 할당한다.
하지만:
- Consumer 내부에서 병렬 처리하면
- 처리 완료 순서가 바뀔 수 있다.
즉,
전달 순서 ≠ 처리 완료 순서
라는 점을 반드시 이해해야 한다.
3️⃣ 재시도와 리밸런싱을 고려해야 한다
- Consumer 재시작
- 파티션 리밸런싱
- ACK 실패
- Visibility timeout 만료
이 상황에서는
이미 처리한 메시지가 다시 오거나
체감 순서가 뒤바뀔 수 있다.
브로커는 90%를 해결해주지만
100%는 아니다.
그래도 깨지는 경우 — 외부 이벤트 시스템
지금까지는 우리가 직접 제어할 수 있는
메시지 브로커를 기준으로 설명했다.
하지만 현실에서는
외부 시스템이 보내는 이벤트도 처리해야 한다.
예를 들면:
- 결제 대행사(PG) 웹훅
- 물류사 배송 상태 콜백
- 외부 인증 서비스 이벤트
- SaaS 서비스 상태 변경 알림
이러한 이벤트는
Kafka나 SQS처럼 세밀하게 통제할 수 없다.
대부분 HTTP Webhook 형태로 전달되며
순서를 보장하지 않는다.
사례 — AWS IVS
AWS IVS는
Amazon Interactive Video Service의 약자로
라이브 스트리밍을 제공하는 AWS 서비스다.
스트리밍 상태가 바뀌면
다음과 같은 이벤트를 보낸다.
- Stream Start
- Stream End
- Recording Ready
이 이벤트들은
순서를 보장하지 않는다.
즉:
- Stream End가 먼저 오고
- Stream Start가 나중에 올 수 있다.
네트워크 지연, 재시도, 내부 처리 순서 등
여러 요인이 작용하기 때문이다.
이 경우 브로커 설정으로 해결할 수 없다.
애플리케이션 레벨 대응 전략
브로커는 1차 방어선이다.
최종 방어선은 애플리케이션이다.
1️⃣ 버전 기반 처리
각 이벤트에 version을 둔다.
- 현재 version보다 낮으면 무시
- 더 높은 version만 반영
상태가 뒤로 가지 않도록 한다.
2️⃣ 현재 상태 기준 검증
이미 CANCELED 상태라면
PAYMENT_COMPLETED 이벤트를 무시한다.
현재 상태가
이벤트를 수용할 수 있는지 판단한다.
3️⃣ 타임스탬프 비교
이벤트 발생 시간을 비교해
더 오래된 이벤트는 폐기한다.
단, 시간 동기화 문제는 고려해야 한다.
4️⃣ 단방향 상태 설계
상태가 뒤로 돌아가지 않도록 설계한다.
예:
CREATED → PAYMENT_PENDING → COMPLETED
이미 COMPLETED라면
이전 단계 이벤트는 무시한다.
순서를 완벽히 보장하려 하지 말라
브로커 설정으로
대부분의 순서 문제는 해결할 수 있다.
하지만 설계 원칙은 이것이다.
순서가 깨져도 시스템이 무너지지 않도록 설계하라.
브로커는 편의를 제공한다.
안전성은 설계가 만든다.
이 장의 핵심
이벤트 기반 시스템에서는
- 순서는 범위 제한적으로만 보장된다.
- 키 단위 스트림 설계가 중요하다.
- 해당 스트림은 동시에 하나만 처리해야 한다.
- 전달 순서와 처리 완료 순서는 다를 수 있다.
- 외부 이벤트는 순서를 보장하지 않는다.
- 애플리케이션이 최종 방어를 해야 한다.